package ru.fadeev.crammer.data;

import java.io.File;
import java.io.RandomAccessFile;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import ru.fadeev.crammer.DatabaseManager;
import ru.fadeev.crammer.GFIndicesStorage;
import ru.fadeev.crammer.IndexerPhraseTask;
import ru.fadeev.crammer.ProgressBar;
import ru.fadeev.crammer.Settings;
import ru.fadeev.crammer.TextFileFilter;
import ru.fadeev.crammer.Utils;


/**
 * PhrasesBatch class represents PhrasesBatch entity. PhrasesBatch is a set
 * of Phrases. All Phrases containted in PhrasesCorpus divided in batches 
 * just for user conviniency. User can make some operations with batches: 
 * view their name, create date, number of phrases it contains, add new, edit
 * or delete batches.
 * @author Konstantin Fadeyev
 *
 */
public class PhrasesBatch {
	private long createDate;
	private int id;
	private String name;
	private Vector<Phrase> phrases;	
	private int phrasesCount;
	
	
	/**
	 * Creates PhrasesBatch with default initial values.
	 */
	public PhrasesBatch() {
		id = -1;
		createDate = System.currentTimeMillis();
		phrasesCount = 0;
		phrases = null;
	}
	
	
	/**
	 * Creates PhrasesBatch with given database id, name and create date
	 * @param id PhrasesBatch database id 
	 * @param name PhrasesBatch name
	 * @param createDate PhrasesBatch create date
	 */
	public PhrasesBatch(int id, String name, long createDate) {
		this.id = id;
		this.name = name;
		this.createDate = createDate;	
		phrases = null;
	}

	
	/**
	 * Adds given phrase to database. Checks length of phrase and
	 * number of words in it.
	 * @param phrase phrase to add to statement
	 * @param statement statement with one string parameter that adds phrase
	 * to database
	 * @param progressBar progress bar
	 * @return true, if phrase was added successfully
	 */
	private boolean addPhraseToDB(String phrase, PreparedStatement statement, 
    		ProgressBar progressBar) {
    	int wordsCount = 0;
    	
		Pattern p = Pattern.compile("[\\w]+");
		Matcher matcher = p.matcher(phrase);
		while(matcher.find()) {
			wordsCount++;
		}
		if (wordsCount < Settings.getInt("addPhraseMinLength")) {
			return false;
		}
		if(phrase.trim().length() > Settings.PHRASE_ADDING_MAX_LENGTH) {
			return false;
		}		
			
		try {
			String compiledPhrase = phrase.trim().replaceAll("\\s+", " ");
			
			compiledPhrase = compiledPhrase.replaceAll("\\A[\"\\s]+", "");
			compiledPhrase = compiledPhrase.replaceAll("[\"\\s]+\\z", "");
			
			statement.setString(1, compiledPhrase);
			statement.executeUpdate();
			return true;
		} catch (Exception e) {
	        e.printStackTrace();
	        return false;
	    }						
    }

	
	/**
	 * Deletes PhrasesBatch and all related information from database.
	 * @param storage
	 */
	public void deleteFromDB(GFIndicesStorage storage) {
		try {			
			Connection connection = DatabaseManager.getConnection();      
			Statement statement = connection.createStatement();
			ResultSet resultSet = statement.executeQuery(
        			"SELECT ID " +
        			"FROM PHRASE " +
        			"WHERE BATCH_ID = " + id
        			);	
			synchronized(storage) {
	        	while(resultSet.next())
	        	{
					storage.deleteGFIndices(resultSet.getInt(1), 
							true, connection);
	        	}
				statement.executeQuery(
	        			"DELETE FROM PHRASE " +
	        			"WHERE BATCH_ID = " + id
	        			);	
				statement.executeQuery(
	        			"DELETE FROM PHRASES_BATCH " +
	        			"WHERE ID = " + id
	        			);		        	
			}
			statement.close();
        	connection.close();
		} catch (Exception e) {
	        e.printStackTrace();
	    }		    	
    }
	
	
	/**
	 * Delimiters is used to divide some text on phrases. This function
	 * returns regular expression (defined by Settings) that represents 
	 * combined delimiter and will be used by regex classes.
	 * @return delimiter regular expression.
	 */
	public String getCombinedDelimiter() {
		String result = "[";
		if(Settings.getBoolean("addPhrasePoint")) {
			result += "\\."; 
		}
		if(Settings.getBoolean("addPhraseExclamation")) {
			result += "\\!"; 
		}
		if(Settings.getBoolean("addPhraseQuestion")) {
			result += "\\?"; 
		}
		if(Settings.getBoolean("addPhraseLF")) {
			result += "\\n"; 
		}
		if(!Settings.getString("addPhraseCustomDelimiter").equals("")) {
			result += Settings.getString("addPhraseCustomDelimiter"); 
		}
		result += "]";
		return result;
	}
	
	
	/**
	 * Returns PhrasesBatch create date
	 * @return PhrasesBatch create date
	 */
	public long getCreateDate() {
		return createDate;
	}
	
	
	/**
	 * Returns PhrasesBatch database id
	 * @return PhrasesBatch database id
	 */
	public int getId() {
		return id;
	}
	
	
	/**
	 * Returns PhrasesBatch name
	 * @return PhrasesBatch name
	 */
	public String getName() {
		return name;
	}

	
	/**
	 * Returns phrase for this PhrasesBatch
	 * @param index index of phrase in the PhrasesBatch
	 * @return phrase for this PhrasesBatch
	 */
	public Phrase getPhrase(int index) {
		return (Phrase)phrases.elementAt(index);
	}
	
	
	/**
	 * Returns total number of phrases in the PhrasesBatch. If loadStatistics
	 * wasn't called then phrasesCount is 0.
	 * @return total number of phrases in the PhrasesBatch.
	 */
	public int getPhrasesCount() {
		return phrasesCount;
	}
	
	
	/**
	 * Adds phrases from all text files (.txt) from the directory and its 
     * subdirectories to phrases corpus.
	 * @param directory directory to search text files in
	 * @param progressBar progress bar
	 * @param storage object of GFIndicesStorage
	 * @return number of added phrases
	 */
	public int insertDirectoryToDB(File directory,  
    		ProgressBar progressBar, GFIndicesStorage storage) {   	
   	    TextFileFilter fileFilter = new TextFileFilter();
   	    
   		File[] files = directory.listFiles(fileFilter);
   		int phrasesCount = 0;
   		for (int i = 0; i < files.length; i++) {
   			if(files[i].isDirectory()) {
   				phrasesCount += insertDirectoryToDB(files[i], progressBar, storage);
   			} else {
   				phrasesCount += insertFileToDB(files[i], progressBar, storage);
   			}
   		}    	
   		return phrasesCount;
    }
	
	
	/**
	 * Adds phrases from the text file (.txt) to phrases corpus.
	 * @param file file to search for phrases to 
	 * @param progressBar progress bar
	 * @param storage object of GFIndicesStorage
	 * @return number of added phrases
	 */
    public int insertFileToDB(File file, 
    		ProgressBar progressBar, GFIndicesStorage storage) {
		
    	// Files longer than PHRASES_FILE_MAX_SIZE (100Mb) 
    	//are not being processed
    	if(file.length() > Settings.PHRASES_FILE_MAX_SIZE) {
    		return 0;
    	}
    	
    	try {   	
    		RandomAccessFile randomAccess = new RandomAccessFile(file,"r");
    		byte[] array = new byte[(int)file.length()];

    		randomAccess.read(array);
    		
    		String content = new String(array);
    		int phraseCount = insertTextToDB(new String(content), 
    				progressBar, storage);
    		randomAccess.close();
    		return phraseCount;
    		
    	} catch (Exception e) {
    		e.printStackTrace();
    		return 0;
    	}		
    }

    
    /**
     * Inserts phrases into the batch without inserting it into the database
     * @param phrase phrase to insert into phrasesBatch
     * @param index index where to insert phrase
     */
    public void insertPhrase(Phrase phrase, int index) {
		phrases.insertElementAt(phrase, index); 
	}
    
    
    /**
     * Adds phrases marked out from the given text to phrases corpus.
     * @param text text to search for phrases in
     * @param progressBar progress bar
     * @param storage object of GFIndicesStorage
     * @return number of added phrases
     */
    public int insertTextToDB(String text, 
    		ProgressBar progressBar, GFIndicesStorage storage) { 
    	
    	int phrasesCount = 0;
    	String currentPhrase;
    	String delimiter = getCombinedDelimiter();
    	
    	// Constructing delimiter 	
    	delimiter += "|(\\r\\n\\r\\n)";   	
    	int currentIndex = 0;
    	int progressBarCoefficient = 3;
    	if(Settings.getBoolean("addPhrasePreprocessing")) {
    		text = prepareText(text, progressBar);
    		progressBarCoefficient = 1;
    	}
    	
    	try {
	    	Connection connection = DatabaseManager.getConnection();  
	    	PreparedStatement statement = connection.prepareStatement(
	    			"INSERT INTO PHRASE " +
	    			"(BATCH_ID, PHRASE_TEXT) " +
	    			"VALUES ("+ id +",?)");
	    	
	    	CallableStatement identityStatement = 
	    		connection.prepareCall("{?= CALL IDENTITY()}");
	    	
			Pattern p = Pattern.compile(delimiter);
			Matcher matcher = p.matcher(text);
			int progressBarValue = progressBar.getPBValue();
			while(matcher.find()) {
				
				// Check for points that don't end sentence
				if(Settings.getBoolean("addPhrasePoint")) {
					currentPhrase = text.substring(currentIndex, 
							matcher.start()+1);

					if(currentPhrase.length() >= 5) {
						String subPhrase = currentPhrase.substring(
								currentPhrase.length()-5, 
								currentPhrase.length());
						String exceptionList = "(\\WMrs\\.)|(\\WEsq\\.)";
							
							if(subPhrase.matches(exceptionList)) {
								continue;
							}
					} 
					if(currentPhrase.length() >= 4) {
						String subPhrase = currentPhrase.substring(
								currentPhrase.length()-4, 
								currentPhrase.length());
						String exceptionList = 
							"(\\WMr\\.)|(\\WMs\\.)|(\\WSt\\.)|" 
								+ "(\\WPh\\.)|(\\WJr\\.)";
							
						if(subPhrase.matches(exceptionList)) {
							continue;
						}
					}
					if(currentPhrase.length() >= 3) {
						String subPhrase = currentPhrase.substring(
								currentPhrase.length()-3, 
								currentPhrase.length());
						String exceptionList = 
							"(\\W[A-Z]\\.)|(\\Wi\\.)|(\\We\\.)";
						if(subPhrase.matches(exceptionList)) {
							continue;
						}
					}	
					if(currentPhrase.length() == 2) {
						String exceptionList = "([A-Z]\\.)";
						if(currentPhrase.matches(exceptionList)) {
							continue;
						}
					}	
				}
				currentPhrase = text.substring(currentIndex, matcher.start());
				currentIndex = matcher.end();
				if (addPhraseToDB(currentPhrase, statement, progressBar)) {
					ResultSet resultSet = identityStatement.executeQuery();	
					resultSet.next();
					int id = resultSet.getInt(1);
					IndexerPhraseTask phraseTask = new IndexerPhraseTask( 
							id, currentPhrase, true);
					storage.addGFIndexerTask(phraseTask);					
					phrasesCount++;
				}
				progressBar.updatePBValue(progressBarValue 
						+ progressBarCoefficient * currentIndex);
			}
			currentPhrase = text.substring(currentIndex, text.length());
			if (addPhraseToDB(currentPhrase, statement, progressBar)) {
				ResultSet resultSet = identityStatement.executeQuery();	
				resultSet.next();
				int id = resultSet.getInt(1);
				IndexerPhraseTask phraseTask = new IndexerPhraseTask(
						id, currentPhrase, true);
				storage.addGFIndexerTask(phraseTask);
				phrasesCount++;
			}
			progressBar.updatePBValue(progressBarValue 
					+ progressBarCoefficient * text.length());
			
			identityStatement.close();
			statement.close();
			connection.close();
    	} catch (Exception e) {
	        e.printStackTrace();
	    }			
    	return phrasesCount;
    }
    
    
    /**
     * Inserts this batch to database. Inserts only batch record, 
     * doesn't insert any phrases.
     */
	public void insertToDB() { 	
    	try {
    		SimpleDateFormat dateFormat = 
    			new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    		
			Connection connection = DatabaseManager.getConnection();      
			Statement statement =
				connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
			          ResultSet.CONCUR_UPDATABLE);
			statement.executeUpdate("INSERT INTO PHRASES_BATCH " +
					"(NAME, " +
						"CREATE_DATE) " +
					"VALUES ('" + Utils.prep(name, 
							Settings.BATCH_NAME_MAX_LENGTH) + "', '" + 
						dateFormat.format(new Date(createDate)) + "')");
	    	statement.close();
			
			CallableStatement identityStatement = 
				connection.prepareCall("{?= CALL IDENTITY()}");
			ResultSet resultSet = identityStatement.executeQuery();	
			resultSet.next();
			id = resultSet.getInt(1);			
			identityStatement.close();
			
	    	connection.close();	    	

    	} catch (Exception e) {
	        e.printStackTrace();
	    }	
    }
	
	
	/**
	 * Loads phrases of this batch from database.
	 */
	public void loadPhrases() {
		phrases = new Vector<Phrase>();
		try {
			Connection connection = DatabaseManager.getConnection();      
			Statement statement = connection.createStatement();;
			ResultSet resultSet = statement.executeQuery(
        			"SELECT ID, PHRASE_TEXT " +
        			"FROM PHRASE " +
        			"WHERE BATCH_ID = " + id
        			);
        	while(resultSet.next())
        	{
        		Phrase phrase = new Phrase(resultSet.getInt(1), resultSet.getString(2));
        		phrases.addElement(phrase);
        	}
        	statement.close();
        	connection.close();
		} catch (Exception e) {
	        e.printStackTrace();
	    }	
	}

	
	/**
	 * Loads some statistics of phrasesBatch without loading phrases.
	 * Right now statistics is only phrasesCount.
	 * @param connection
	 */
	void loadStatistics(Connection connection) {
		try {      
			Statement statement = connection.createStatement();
			ResultSet resultSet = statement.executeQuery(
        			"SELECT COUNT(*) " +
        			"FROM PHRASE " +
        			"WHERE BATCH_ID = " + id
        			);	
        	resultSet.next();
        	phrasesCount = resultSet.getInt(1);
        	statement.close();
		} catch (Exception e) {
	        e.printStackTrace();
	    }	
	}
	
	
	/**
	 * Prepares text for searching for phrases in it by removing some junk
	 * from text (like blank lines or title lines).
	 * @param text text to prepare for phrases searching
	 * @param progressBar progress bar
	 * @return text without junk
	 */
	private String prepareText(String text, ProgressBar progressBar){
    	// if there is single line arounded by blank lines 
    	// and not ended by '.' (it means it's title and not processed) 
    	String blankLine = "(\\r\\n(\\s)*\\r\\n)|(\\n(\\s)*\\n)";
    	String result="";
    	int currentIndex = 0;
		Pattern blankLinePattern = Pattern.compile(blankLine);
		Pattern lineFeedPattern = Pattern.compile("[\\r\\n]");
		Matcher blankLineMatcher = blankLinePattern.matcher(text);

		int progressBarValue = progressBar.getPBValue();
		while(blankLineMatcher.find()) {					
			String currentPhrase = text.substring(currentIndex, blankLineMatcher.start());
			Matcher lineFeedMatcher = lineFeedPattern.matcher(currentPhrase);
			if(lineFeedMatcher.find(0)) {
				result += text.substring(currentIndex, blankLineMatcher.end());
			}else if (currentPhrase.endsWith(".")){
				result += text.substring(currentIndex, blankLineMatcher.end());
			}
			currentIndex = blankLineMatcher.end();
			progressBar.updatePBValue(progressBarValue + currentIndex);
		}
		result += text.substring(currentIndex, text.length());
		progressBarValue += text.length();
		progressBar.updatePBValue(progressBarValue);
    	String result2="";
    	currentIndex = 0;
		Pattern openBracketPattern = Pattern.compile("\\(");
		Pattern closeBracketPattern = Pattern.compile("\\)");
		Matcher openBracketMatcher = openBracketPattern.matcher(result);
		Matcher closeBracketMatcher = closeBracketPattern.matcher(result);
		while(openBracketMatcher.find(currentIndex)) {
			result2 += result.substring(currentIndex, openBracketMatcher.start());			
			if(closeBracketMatcher.find(openBracketMatcher.end())) {
				currentIndex = closeBracketMatcher.end();
			} else {
				currentIndex = openBracketMatcher.end();
			}
			progressBar.updatePBValue(progressBarValue + currentIndex);
		}
		result2 += result.substring(currentIndex, result.length());
		progressBar.updatePBValue(progressBarValue + text.length());			
		return result2;
    }
	
	
	/**
	 * Removes phrase from PhrasesBatch. Doesn't remove it from database.
	 * @param index index of phrase to remove.
	 */
	public void removePhrase(int index) {
		if (phrases == null) {
			loadPhrases();
		}
		phrases.remove(index);
	}
	
	
	/**
	 * Removes phrases from PhrasesBatch. Doesn't remove them from database.
	 * @param indices indices of phrases to remove
	 */
	public void removePhrases(int[] indices) {
		for(int i = indices.length-1; i >= 0; i--) {
			phrases.remove(indices[i]);
			phrasesCount--;
		}
	}

	
	/**
	 * Removes phrases specified by database ids from database
	 * @param deletedIds ids of phrases to delete	
	 * @param storage object of GFIndicesStorage 
	 */
	public void removePhrasesFromDB(Vector<Integer> deletedIds, 
			GFIndicesStorage storage) {
		try {
	    	Connection connection = DatabaseManager.getConnection();  
	    	PreparedStatement deleteStatement = 
	    		connection.prepareStatement(
	    			"DELETE FROM PHRASE " +
	    			"WHERE ID = ?");
	    	synchronized(storage) {
				for(int i=0; i<deletedIds.size(); i++) {
					Integer deletedId = (Integer)deletedIds.elementAt(i);
					storage.deleteGFIndices(deletedId.intValue(), 
							true, connection);
					deleteStatement.setInt(1, deletedId.intValue());
					deleteStatement.addBatch();					
				}
				deleteStatement.executeBatch();
				deleteStatement.close();
	    	}
			connection.close();
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	
	/**
	 * Save phrases to database depending on its status. This function is
	 * used when user edits phrases in EditBatchDialog. All changes are save
	 * into phrases list of this batch and altered phrases status are changed
	 * to INSERTED or UPDATED. 
	 * @param storage object of GFIndicesStorage
	 */
	public synchronized void savePhrases(GFIndicesStorage storage) {
		try {
	    	Connection connection = DatabaseManager.getConnection();  
	    	PreparedStatement insertStatement = connection.prepareStatement(
	    			"INSERT INTO PHRASE " +
	    			"(BATCH_ID, PHRASE_TEXT) " +
	    			"VALUES ("+ id +",?)");

	    	PreparedStatement updateStatement = connection.prepareStatement(
	    			"UPDATE PHRASE " +
	    			"SET PHRASE_TEXT = ? " +
	    			"WHERE ID = ?");
	    	
	    	CallableStatement identityStatement = 
	    		connection.prepareCall("{?= CALL IDENTITY()}");

			for(int i=0; i<size(); i++) {
				Phrase phrase = getPhrase(i);
				if(phrase.getStatus() == Phrase.INSERTED) {
					insertStatement.setString(1, phrase.getText());
					insertStatement.executeUpdate();
					
					ResultSet resultSet = identityStatement.executeQuery();	
					resultSet.next();
					int phraseId = resultSet.getInt(1);
					phrase.setId(phraseId);
					IndexerPhraseTask phraseTask = new IndexerPhraseTask( 
							phrase.getId(), phrase.getText(), true);
					storage.addGFIndexerTask(phraseTask);
				} else if (phrase.getStatus() == Phrase.UPDATED) {					
					updateStatement.setString(1, phrase.getText());
					updateStatement.setInt(2, phrase.getId());
					updateStatement.addBatch();		
					IndexerPhraseTask phraseTask = new IndexerPhraseTask( 
							phrase.getId(), phrase.getText(), false);
					storage.addGFIndexerTask(phraseTask);
				}
			}
			insertStatement.close();
			updateStatement.executeBatch();
			updateStatement.close();
			identityStatement.close();
			connection.close();
			
			phrasesCount = size();
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	
	/**
	 * Sets new create date for PhrasesBatch
	 * @param value new create date for PhrasesBatch
	 */
    protected void setCreateDate(long value) {
		createDate = value;
	}
    
    
    /**
     * Sets new database id for PhrasesBatch
     * @param value new database id for PhrasesBatch
     */
    protected void setId(int value) {
		id = value;
	}
     
    
    /**
     * Sets new name for PhrasesBatch
     * @param value new name for PhrasesBatch
     */
	public void setName(String value) {
		name = value;
	}
	
	
	/**
	 * Sets new phrases count for PhrasesBatch
	 * @param value new phrases count for PhrasesBatch
	 */
    public void setPhrasesCount(int value) {
		phrasesCount = value;
	}
   
    
    /**
     * Returns size of phrases list
     * @return size of phrases list
     */
    public int size() {
		return phrases.size();
	}
    
    
    /**
     * Clears phrases list
     */
    public void unloadPhrases() {
		phrases = null;
	}
    
    
    /**
     * Updates name of PhrasesBatch in database
     */
	public void update() { 	
    	try {
			Connection connection = DatabaseManager.getConnection();      
			Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
			          ResultSet.CONCUR_UPDATABLE);
			statement.executeUpdate("UPDATE PHRASES_BATCH " +
					"SET NAME = '" + Utils.prep(name, 
							Settings.BATCH_NAME_MAX_LENGTH) + "'" +
					"WHERE ID = " + id);
	    	statement.close();
	    	connection.close();	    	

    	} catch (Exception e) {
	        e.printStackTrace();
	    }	
    }	
}
